#include "shell.h"
#include "file.h"
#include "debug.h"
#include "error.h"
#include "logging.h"
#include "system.h"
#include "string.h"
#include "path.h"
#include "reg_key.h"
#include "app_util.h"
#include "const_utils.h"
#include "utils.h"
#include "browser_utils.h"

// create and store a shortcut
// uses shell IShellLink and IPersistFile interfaces
HRESULT Shell::CreateLink(const TCHAR *source, const TCHAR *destination, 
						  const TCHAR *working_dir, const TCHAR *arguments,
						  const TCHAR *description, WORD hotkey_virtual_key_code, 
						  WORD hotkey_modifiers, const TCHAR *icon)
{
	ASSERT1(source);
	ASSERT1(destination);
	ASSERT1(working_dir);
	ASSERT1(arguments);
	ASSERT1(description);

	scoped_co_init co_init(COINIT_APARTMENTTHREADED);
	HRESULT hr = co_init.hresult();
	if (FAILED(hr) && hr != RPC_E_CHANGED_MODE)
	{
		UTIL_LOG(LEVEL_ERROR, (_T("[Shell::CreateLink - failed to co_init]"), hr));
		return hr;
	}

	UTIL_LOG(L1, (_T("[Create shell link]")
		_T("[source %s dest %s dir %s arg %s desc %s hotkey %x:%x]"),
		source, destination, working_dir, arguments, description,
		hotkey_modifiers, hotkey_virtual_key_code));

	// Get a pointer to the IShellLink interface
	CComPtr<IShellLink> shell_link;

	RET_IF_FAILED(shell_link.CoCreateInstance(CLSID_ShellLink));
	ASSERT(shell_link, (L""));

	// Set the path to the shortcut target and add the description
	VERIFY1(SUCCEEDED(shell_link->SetPath(source)));
	VERIFY1(SUCCEEDED(shell_link->SetArguments(arguments)));
	VERIFY1(SUCCEEDED(shell_link->SetDescription(description)));
	VERIFY1(SUCCEEDED(shell_link->SetWorkingDirectory(working_dir)));

	// If we are given an icon, then set it
	// For now, we always use the first icon if this happens to have multiple ones
	if (icon) {
		VERIFY1(SUCCEEDED(shell_link->SetIconLocation(icon, 0)));
	}

	// C4201: nonstandard extension used : nameless struct/union
#pragma warning(disable : 4201)
	union {
		WORD flags;
		struct {                  // little-endian machine:
			WORD virtual_key:8;     // low order byte
			WORD modifiers:8;       // high order byte
		};
	} hot_key;
#pragma warning(default : 4201)

	hot_key.virtual_key = hotkey_virtual_key_code;
	hot_key.modifiers = hotkey_modifiers;

	if (hot_key.flags) {
		shell_link->SetHotkey(hot_key.flags);
	}

	// Query IShellLink for the IPersistFile interface for saving the shortcut in
	// persistent storage
	CComQIPtr<IPersistFile> persist_file(shell_link);
	if (!persist_file)
		return E_FAIL;

	// Save the link by calling IPersistFile::Save
	RET_IF_FAILED(persist_file->Save(destination, TRUE));

	return S_OK;
}

HRESULT Shell::RemoveLink(const TCHAR *link)
{
	ASSERT(link, (L""));
	ASSERT(*link, (L""));

	return File::Remove(link);
}

// Open a URL in a new browser window
HRESULT Shell::OpenLinkInNewWindow(const TCHAR* url, UseBrowser use_browser)
{
	ASSERT1(url);

	HRESULT hr = S_OK;
	CString browser_path;

	// Try to open with default browser
	if (use_browser == USE_DEFAULT_BROWSER)
	{
		// Load full browser path from regkey
		hr = GetDefaultBrowserPath(&browser_path);

		// If there is a default browser and it is not AOL, load the url in that
		if (SUCCEEDED(hr) && String_Contains(browser_path, _T("aol")))
		{
			if (!browser_path.IsEmpty())
			{
				// Have we figured out how to append the URL onto the browser path?
				bool acceptable_url = false;
				
				if (ReplaceCString(browser_path, _T("\"%1\""), url))	// the "browser.exe "%1"" case
					acceptable_url = true;
				else if (ReplaceCString(browser_path, _T("%1"), url))	// the "browser.exe %1" case
					acceptable_url = true;
				else if (ReplaceCString(browser_path, _T("-nohome"), url))	// the "browser.exe -nohome" case
					acceptable_url = true;
				else
				{
					EnclosePath(NULL);
					browser_path.AppendChar(' ');
					CString quoted_url(url);
					EnclosePath(&quoted_url);
					browser_path.Append(quoted_url);
					acceptable_url = true;
				}

				if (acceptable_url)
				{
					hr = System::ShellExecuteCommandLine(browser_path, NULL, NULL);
					if (SUCCEEDED(hr))
						return S_OK;
					else					
						UTIL_LOG(LE, (_T("[Shell::OpenLinkInNewWindow]")
							_T("[failed to start default browser to open url]")
							_T("[%s][0x%x]"), url, hr));
				}
			}
		}
	}

	// Try to open with IE if can't open with default browser or required
	if (use_browser == USE_DEFAULT_BROWSER ||
		use_browser == USE_INTERNET_EXPLORER)
	{
		hr = RegKey::GetValue(kRegKeyIeClass, kRegValueIeClass, &browser_path);
		if (SUCCEEDED(hr)) {
			hr = System::ShellExecuteProcess(browser_path, url, NULL, NULL);
			if (SUCCEEDED(hr)) 
				return S_OK;
			else 
				UTIL_LOG(LE, (_T("[Shell::OpenLinkInNewWindow]")
					_T("[failed to start IE to open url][%s][0x%x]"),
					url, hr));			
		}
	}

	// Try to open with Firefox if can't open with default browser or required
	if (use_browser == USE_DEFAULT_BROWSER || use_browser == USE_FIREFOX)
	{
		hr = RegKey::GetValue(kRegKeyFirefox, kRegValueFirefox, &browser_path);
		if (SUCCEEDED(hr) && !browser_path.IsEmpty()) {
			ReplaceCString(browser_path, _T("%1"), url);
			hr = System::ShellExecuteCommandLine(browser_path, NULL, NULL);
			if (SUCCEEDED(hr)) 
				return S_OK;
			else
				UTIL_LOG(LE, (_T("[Shell::OpenLinkInNewWindow]")
					_T("[failed to start Firefox to open url][%s][0x%x]"),
					url, hr));			
		}
	}

	// ShellExecute the url directly as a last resort
	hr = Shell::Execute(url);
	if (FAILED(hr))
		UTIL_LOG(LE, (_T("[Shell::OpenLinkInNewWindow]")
			_T("[failed to run ShellExecute to open url][%s][0x%x]"),
			url, hr));

	return hr;
}

HRESULT Shell::Execute(const TCHAR* file)
{
	ASSERT1(file);

	// Prepare everything required for ::ShellExecuteEx().
	SHELLEXECUTEINFO sei;
	SetZero(sei);
	sei.cbSize = sizeof(sei);
	sei.fMask = SEE_MASK_FLAG_NO_UI     |  // Do not display an error message box.
				SEE_MASK_NOZONECHECKS   |  // Do not perform a zone check.
				SEE_MASK_NOASYNC;          // Wait to complete before returning.
	// Pass NULL for hwnd. This will have ShellExecuteExEnsureParent()
	// create a dummy parent window for us.
	// sei.hwnd = NULL;
	sei.lpVerb = _T("open");
	sei.lpFile = file;
	// No parameters to pass
	// sei.lpParameters = NULL;
	// Use parent's starting directory
	// sei.lpDirectory = NULL;
	sei.nShow = SW_SHOWNORMAL;

	// Use ShellExecuteExEnsureParent to ensure that we always have a parent HWND.
	// We need to use the HWND Property to be acknowledged as a Foreground
	// Application on Vista. Otherwise, the elevation prompt will appear minimized
	// on the taskbar.
	if (!ShellExecuteExEnsureParent(&sei))
	{
		HRESULT hr(HRESULTFromLastError());
		ASSERT(false, (_T("Shell::Execute - ShellExecuteEx failed][%s][0x%x]"), file, hr));
		return hr;
	}

	return S_OK;
}

HRESULT Shell::BasicGetSpecialFolder(DWORD csidl, CString* folder_path)
{
	ASSERT1(folder_path);

	// Get a ITEMIDLIST* (called a PIDL in the MSDN documentation) to the
	// special folder
	scoped_any<ITEMIDLIST*, close_co_task_free> folder_location;
	RET_IF_FAILED(::SHGetFolderLocation(NULL,
		csidl,
		NULL,
		0,
		address(folder_location)));
	ASSERT(get(folder_location), (_T("")));

	// Get an interface to the Desktop folder
	CComPtr<IShellFolder> desktop_folder;
	RET_IF_FAILED(::SHGetDesktopFolder(&desktop_folder));
	ASSERT1(desktop_folder);

	// Ask the desktop for the display name of the special folder
	STRRET str_return;
	SetZero(str_return);
	str_return.uType = STRRET_WSTR;
	RET_IF_FAILED(desktop_folder->GetDisplayNameOf(get(folder_location),
		SHGDN_FORPARSING,
		&str_return));

	// Get the display name of the special folder and return it
	scoped_any<wchar_t*, close_co_task_free> folder_name;
	RET_IF_FAILED(::StrRetToStr(&str_return,
		get(folder_location),
		address(folder_name)));
	*folder_path = get(folder_name);

	return S_OK;
}

HRESULT Shell::GetSpecialFolder(DWORD csidl,
								bool create_if_missing,
								CString* folder_path)
{
	ASSERT(folder_path, (L""));

	HRESULT hr = Shell::BasicGetSpecialFolder(csidl, folder_path);

	// If the folder does not exist, ::SHGetFolderLocation may return error
	// code ERROR_FILE_NOT_FOUND.
	if (create_if_missing) {
		if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
			(SUCCEEDED(hr) && !File::Exists(*folder_path))) {
				hr = Shell::BasicGetSpecialFolder(csidl | CSIDL_FLAG_CREATE, folder_path);
		}
	}
	ASSERT(FAILED(hr) || File::Exists(*folder_path), (_T("")));

	return hr;
}

#pragma warning(disable : 4510 4610)
// C4510: default constructor could not be generated
// C4610: struct can never be instantiated - user defined constructor required
struct {
	const TCHAR* name;
	const DWORD csidl;
} const folder_mappings[] = {
	L"APPDATA",            CSIDL_APPDATA,
	L"DESKTOP",            CSIDL_DESKTOPDIRECTORY,
	L"LOCALAPPDATA",       CSIDL_LOCAL_APPDATA,
	L"MYMUSIC",            CSIDL_MYMUSIC,
	L"MYPICTURES",         CSIDL_MYPICTURES,
	L"PROGRAMFILES",       CSIDL_PROGRAM_FILES,
	L"PROGRAMFILESCOMMON", CSIDL_PROGRAM_FILES_COMMON,
	L"PROGRAMS",           CSIDL_PROGRAMS,
	L"STARTMENU",          CSIDL_STARTMENU,
	L"STARTUP",            CSIDL_STARTUP,
	L"SYSTEM",             CSIDL_SYSTEM,
	L"WINDOWS",            CSIDL_WINDOWS,
};
#pragma warning(default : 4510 4610)

HRESULT Shell::GetSpecialFolderKeywordsMapping(
	std::map<CString, CString>* special_folders_map)
{
	ASSERT1(special_folders_map);

	special_folders_map->clear();

	for (size_t i = 0; i < arraysize(folder_mappings); ++i)
	{
		CString name(folder_mappings[i].name);
		DWORD csidl(folder_mappings[i].csidl);
		CString folder;
		HRESULT hr = GetSpecialFolder(csidl, false, &folder);
		if (FAILED(hr))
		{
			UTIL_LOG(LE, (_T("[Shell::GetSpecialFolderKeywordsMapping]")
				_T("[failed to retrieve %s]"), name));
			continue;
		}
		special_folders_map->insert(std::make_pair(name, folder));
	}

	// Get the current module directory
	CString module_dir = app_util::GetModuleDirectory(::GetModuleHandle(NULL));
	ASSERT1(module_dir.GetLength() > 0);
	special_folders_map->insert(std::make_pair(_T("CURRENTMODULEDIR"),
		module_dir));

	return S_OK;
}

HRESULT Shell::DeleteDirectory(const TCHAR* dir)
{
	ASSERT1(dir && *dir);

	if (!SafeDirectoryNameForDeletion(dir)) {
		return E_INVALIDARG;
	}

	uint32 dir_len = lstrlen(dir);
	if (dir_len >= MAX_PATH) {
		return E_INVALIDARG;
	}

	// the 'from' must be double-terminated with 0. Reserve space for one more
	// zero at the end
	TCHAR from[MAX_PATH + 1] = {0};
	lstrcpyn(from, dir, MAX_PATH);
	from[1 + dir_len] = 0;    // the second zero terminator.

	SHFILEOPSTRUCT file_op = {0};

	file_op.hwnd   = 0;
	file_op.wFunc  = FO_DELETE;
	file_op.pFrom  = from;
	file_op.pTo    = 0;
	file_op.fFlags = FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;

	// ::SHFileOperation returns non-zero on errors
	return ::SHFileOperation(&file_op) ? HRESULTFromLastError() : S_OK;
}

HRESULT Shell::GetApplicationExecutablePath(const CString& exe,
											CString* path)
{
	ASSERT1(path);

	CString reg_key_name = AppendRegKeyPath(kRegKeyApplicationPath, exe);
	return RegKey::GetValue(reg_key_name, kRegKeyPathValue, path);
}